package com.base.utils;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.io.FileUtils;

import com.base.entity.RedisObject;
import com.base.utils.constract.DataBackupFileConstract;
import com.base.utils.resolver.BackupFileResolver;

import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

/**
 * redeis tool
 * @author huwei
 *
 */
@Slf4j
public class RedisTool {

	private BackupFileResolver backupFileResolver = new BackupFileResolver();
	
	private String host;
	
	private Integer port;
	
	private String password;
	
	private int db;
	
	private Jedis jedis;
	
	public RedisTool(String host ,int port) {
		this(host ,port ,0);
	}
	
	public RedisTool(String host ,int port ,int db) {
		this(host ,port ,null ,db);
	}
	
	public RedisTool(String host ,int port ,String passwrod ,int db) {
		this.host = host ;
		this.port = port ;
		this.db = db;
		this.password = passwrod;
		
		init();
	}

	private void init() {
		jedis = connection();
		jedis.select(db);
	}
	
	public boolean restore(String pathName) {
		return restore(new File(pathName));
	}
	
	public boolean restore(File file) {
		Assert.nonNull(file);
		Assert.isExistsOfFile(file);
		
		try {
			String str = FileUtils.readFileToString(file);
			log.info("read " + file.getAbsolutePath() +" file over ,and ready to decode data !!!");
			restoreRedisObject(backupFileResolver.decode(str));
			log.info("restore data to redis .......");
		} catch (IOException e) {
			log.warn("read "+file.getAbsolutePath()+" file failed !!",e);
		}
		return true;
	}
	
	
	@SuppressWarnings("unchecked")
	private void restoreRedisObject(List<RedisObject> redisObjectList) {
		for(int i = 0 ,len = redisObjectList.size() ;i < len ;++i) {
			RedisObject redisObject = redisObjectList.get(i);
			switch (redisObject.getRedisType().toUpperCase()) {
			case DataBackupFileConstract.TYPE_STRING:
				jedis.set(redisObject.getKey(), (String)redisObject.getData());
				break;
			case DataBackupFileConstract.TYPE_LIST:
				listSet(jedis ,(List<String>)redisObject.getData(), redisObject.getKey());
				break;
			case DataBackupFileConstract.TYPE_SET:
				setAdd(jedis ,(Set<String>)redisObject.getData(), redisObject.getKey());
				break;
			case DataBackupFileConstract.TYPE_ZSET:
				zsetAdd(jedis ,(Set<Tuple>)redisObject.getData(), redisObject.getKey());
				break;
			case DataBackupFileConstract.TYPE_HASH:
				hashPut(jedis ,(Map<String ,String>) redisObject.getData() ,redisObject.getKey());
			}
		}
	}
	
	private void setAdd(Jedis jedis, Set<String> data, String key) {
		Iterator<String> iter = data.iterator();
		while(iter.hasNext()) {
			String value = iter.next();
			jedis.sadd(key, value);
		}
	}
	
	private void hashPut(Jedis jedis, Map<String,String> data, String key) {
		for(Map.Entry<String, String> entry : data.entrySet()) {
			String field = entry.getKey();
			String value = entry.getValue();
			jedis.hset(key, field, value);
		}
	}
	
	private void zsetAdd(Jedis jedis, Set<Tuple> data, String key) {
		Iterator<Tuple> iter = data.iterator();
		while(iter.hasNext()) {
			Tuple tuple = iter.next();
			jedis.zadd(key ,tuple.getScore() ,tuple.getElement());
		}
	}

	private void listSet(Jedis jedis, List<String> data ,String key) {
		for(int i = 0 ,len = data.size() ;i <len ;++i) {
			jedis.lpush(key, data.get(i));
		}
	}

	public boolean backups(String pathName) {
		Assert.nonNull(pathName);
		return backups(new File(pathName));
	}
	
	public boolean backups(File file) {
		Assert.nonNull(file);
		Assert.isExistsOfFile(file);
		
		boolean result = false;
		
		Set<String> keys = jedis.keys("*");
		if(Objects.nonNull(keys) && !keys.isEmpty()) {
			
			log.info("get redis keys is ok ,and redis object have " + keys.size());
			log.info("ready to encode str for redis object ,please wait ");
			
			StringBuffer sb = new StringBuffer();
			Iterator<String> iter = keys.iterator();
			while(iter.hasNext()) {
				String key = iter.next();
				if(key.startsWith("redisCache")) {
					continue;
				}
				String typeOfKey = jedis.type(key);
				RedisObject ro = getRedisObject(jedis, key, typeOfKey);
				sb.append(backupFileResolver.encode(ro));
			}
			
			log.info("redis object encode str success ,and ready to write file ....");
			try {
				FileUtils.write(file, sb);
			} catch (IOException e) {
				log.warn("io exception ,write data in " + file.getAbsolutePath() +" file is failed " ,e);
				throw new RuntimeException("io exception ,write fialed!!");
			}
		}
		else {
			log.debug("redis db not have datas");
		}
		
		return result;
	}
	
	private void connFailed() {
		if(jedis == null) {
			throw new RuntimeException("redis connection failed ,host : " + host + " ; port : " + port);
		}
	}

	private Jedis connection() {
		Jedis jedis = null;
		try {
			jedis = new Jedis(host, port);
			if(password != null) {
				jedis.auth(password);
			}
			if(!"PONG".equals(jedis.ping())) {
				connFailed();
			}
		} catch (Exception e) {
			connFailed();
		}
		
		return jedis;
	}
	
	private RedisObject getRedisObject(Jedis jedis ,String key ,String type) {
		RedisObject ro = new RedisObject();
		ro.setKey(key);
		ro.setRedisType(type.toUpperCase());
		
		switch (type.toUpperCase()) {
		case DataBackupFileConstract.TYPE_STRING:
			String value = jedis.get(key);
			ro.setData(value);
			break;
		case DataBackupFileConstract.TYPE_HASH:
			Map<String, String> map = jedis.hgetAll(key);
			ro.setData(map);
			break;
		case DataBackupFileConstract.TYPE_SET:
			Set<String> set = jedis.smembers(key);
			ro.setData(set);
			break;
		case DataBackupFileConstract.TYPE_LIST:
			List<String> list = jedis.lrange(key, 0, -1);
			ro.setData(list);
			break;
		case DataBackupFileConstract.TYPE_ZSET:
			Set<Tuple> zset = jedis.zrangeWithScores(key, 0, -1);
			ro.setData(zset);
		}
		
		return ro;
	}
	
	public void destory() {
		if(jedis != null) {
			jedis.close();
		}
	}
}
